module sample.HordeApp;

public import derelict.horde3d.horde3d;
public import derelict.horde3d.horde3dutils;
public import derelict.sdl.sdl;

import tango.stdc.stringz;
import Float = tango.text.convert.Float;
import tango.io.Stdout;

static this()
{
	DerelictSDL.load();
	DerelictHorde3D.load();
	DerelictHorde3DUtils.load();

	if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
}

static ~this()
{
	SDL_Quit();
}

public char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}

public abstract class HordeApp
{
	private SDL_Surface* screen;
	private bool[SDLK_LAST] keys;
	private bool[] oldKeys;

	private bool running;
	private bool showFPS;
	private bool wireframeMode;
	private bool debugViewMode;
	private bool invertMouse;
	private int frames;
	private float fps, curFPS, timer;
	private float ticks;
	private char[] fpsText;
	
	protected NodeHandle cam;
	protected ResHandle fontMatRes;
	
	public void openWindow(int width = 800, int height = 600, int bitsPerPixel = 32,
		char[] title = "Horde3D Application", bool fullscreen = false)
	{
		ushort[] bpp = bppToArray(bitsPerPixel);
		SDL_GL_SetAttribute(SDL_GL_RED_SIZE, bpp[0]);
		SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, bpp[1]);
		SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, bpp[2]);
		SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 5);
		SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
		SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

		// Set the title.
		SDL_WM_SetCaption(toStringz(title), null);

		// Hide cursor
		SDL_ShowCursor(0);
		
		// Grab the mouse
		SDL_WM_GrabInput(SDL_GRAB_ON);

		// Flags tell SDL about the type of window we are creating.
		int flags = SDL_OPENGL;
		if (fullscreen)
		{
			flags |= SDL_FULLSCREEN;
		}

		// Create the window
		screen = SDL_SetVideoMode(width, height, bitsPerPixel, flags);
		if (screen is null)
		{
			throw new Exception("Failed to create SDL window: " ~ getSDLError());
		}
		
		//SDL doesn't trigger off a ResizeEvent at startup, but as we need this for OpenGL, we do this ourself
		SDL_Event resizeEvent;
		resizeEvent.type = SDL_VIDEORESIZE;
		resizeEvent.resize.w = width;
		resizeEvent.resize.h = height;

		SDL_PushEvent(&resizeEvent);
	}

	public bool init()
	{
		// Initialize engine
		if (!Horde3D.init())
		{
			Horde3DUtils.dumpMessages();
			return false;
		}

		// Set paths for resources
		Horde3DUtils.setResourcePath(ResourceTypes.SceneGraph, "models");
		Horde3DUtils.setResourcePath(ResourceTypes.Geometry, "models");
		Horde3DUtils.setResourcePath(ResourceTypes.Animation, "models");
		Horde3DUtils.setResourcePath(ResourceTypes.Material, "materials");
		Horde3DUtils.setResourcePath(ResourceTypes.Code, "shaders");
		Horde3DUtils.setResourcePath(ResourceTypes.Shader, "shaders");
		Horde3DUtils.setResourcePath(ResourceTypes.Texture2D, "textures");
		Horde3DUtils.setResourcePath(ResourceTypes.TextureCube, "textures");
		Horde3DUtils.setResourcePath(ResourceTypes.Effect, "effects");
		Horde3DUtils.setResourcePath(ResourceTypes.Pipeline, "pipelines");

		// Set options
		Horde3D.setOption(EngineOptions.LoadTextures, 1);
		Horde3D.setOption(EngineOptions.TexCompression, 0);
		Horde3D.setOption(EngineOptions.FastAnimation, 0);
		Horde3D.setOption(EngineOptions.AnisotropyFactor, 8);
		Horde3D.setOption(EngineOptions.ShadowMapSize, 2048);

		// Font
		fontMatRes = Horde3D.addResource(ResourceTypes.Material, "font.material.xml", 0);

		return true;
	}

	public void run()
	{
		timer = 0.0f;
		fps = 30.0f;
		curFPS = 30.0f;
		ticks = SDL_GetTicks();
		running = true;
		invertMouse = true;
		
		while (running)
		{
			// Calc FPS
			calcFPS();
			// Process input
			processEvents();
			// Update logic and animation
			update();
			// Render world
			render();
			// Render FPS
			renderFPS();

			// Render scene
			Horde3D.render(cam);

			// Remove all overlays
			Horde3D.clearOverlays();

			// Write all mesages to log file
			Horde3DUtils.dumpMessages();
			
			SDL_GL_SwapBuffers();
		}
	}

	public void toggleShowFPS()
	{
		showFPS = !showFPS;
	}
	
	public bool isShowingFPS()
	{
		return showFPS;
	}
	
	public bool isInvertMouse()
	{
		return invertMouse;
	}
	
	public void toggleInvertMouse()
	{
		invertMouse = !invertMouse;
	}
	
	public void toggleWireframeMode()
	{
		wireframeMode = !wireframeMode;
		Horde3D.setOption(EngineOptions.WireframeMode, wireframeMode ? 1.0f : 0.0f);
	}
	
	public void toggleDebugViewMode()
	{
		debugViewMode = !debugViewMode;
		Horde3D.setOption(EngineOptions.DebugViewMode, debugViewMode ? 1.0f : 0.0f);
	}
	
	public void release()
	{
		// Release engine
		Horde3D.release();
		
		SDL_WM_GrabInput(SDL_GRAB_OFF);
		
		SDL_ShowCursor(1);
	}
	
	protected abstract void render();
	
	protected void update()
	{
		if (wasKeyPressed(SDLK_F5))
		{
			toggleInvertMouse();
		}
	}

	protected bool isKeyUp(int key)
	{
		return !keys[key];
	}
	
	protected bool isKeyDown(int key)
	{
		return keys[key];
	}
	
	protected bool wasKeyPressed(int key)
	{
		return !keys[key] && oldKeys[key];
	}
	
	protected void mouseMoved(int xrel, int yrel)
	{
	}
	
	protected float getFPS()
	{
		return curFPS;
	}

	protected void setRunning(bool running)
	{
		this.running = running;
	}
	
	protected void resize(int width, int height)
	{
		// Resize viewport
		Horde3D.resize(0, 0, width, height);

		// Set virtual camera parameters
		Horde3D.setupCameraView(cam, 45.0f, cast(float) width / height, 0.1f, 1000.0f);
	}
	
	private void processEvents()
	{
		oldKeys = keys.dup;
		SDL_Event event;
		while (SDL_PollEvent(&event))
		{
			switch (event.type)
			{
				case SDL_KEYUP:
					keys[event.key.keysym.sym] = false;
					break;
				case SDL_KEYDOWN:
					keys[event.key.keysym.sym] = true;
					break;
				case SDL_MOUSEMOTION:
					mouseMoved(event.motion.xrel, event.motion.yrel);
					break;
				case SDL_QUIT:
					running = false;
					break;
				case SDL_VIDEORESIZE:
					// the window has been resized so we need to set up our viewport and projection according to 
					// the new size
					resize(event.resize.w, event.resize.h);
					break;
				default:
					break;
			}
		}
	}
	
	private void calcFPS()
	{
		frames++;
		if(frames >= 3)
		{
			float t = SDL_GetTicks();
			float deltaInSecs = (t - ticks) * 0.001;
			fps = frames / deltaInSecs;
			frames = 0;
			ticks = t;
		}
		curFPS = fps;
		timer += 1 / fps;
	}

	private void renderFPS()
	{
		if (showFPS)
		{
			// Avoid updating FPS text every frame to make it readable
			if (timer > 0.3f)
			{
				fpsText = "FPS: " ~ Float.toString(curFPS);
				timer = 0;
			}

			// Show text
			Horde3DUtils.showText(toStringz(fpsText), 0, 0.95f, 0.03f, 0, fontMatRes);
		}
	}

	private ushort[] bppToArray(ushort bitsPerPixel)
	{
		switch (bitsPerPixel)
		{
			case 8:
				return new ushort[2 , 3 , 3];
			case 15:
				return new ushort[5 , 5 , 5];
			case 16:
				return new ushort[5 , 6 , 5];
			case 24:
			case 32:
				return new ushort[8 , 8 , 8];
			default:
				throw new Exception("Invalid bits per pixel.");
		}
	}
}
